Opanuj pliki deklaracji TypeScript (.d.ts), aby odblokować bezpieczeństwo typów i autouzupełnianie dla każdej biblioteki JavaScript. Dowiedz się, jak używać @types, tworzyć własne definicje i obsługiwać kod innych firm jak profesjonalista.
Odblokowanie ekosystemu JavaScript: Dogłębna analiza plików deklaracji TypeScript
TypeScript zrewolucjonizował nowoczesne tworzenie stron internetowych, wprowadzając typowanie statyczne do dynamicznego świata JavaScript. To bezpieczeństwo typów zapewnia niesamowite korzyści: wychwytywanie błędów w czasie kompilacji, włączanie potężnego autouzupełniania w edytorze i sprawianie, że duże bazy kodu są znacznie łatwiejsze w utrzymaniu. Jednak pojawia się duże wyzwanie, gdy chcemy użyć rozległego ekosystemu istniejących bibliotek JavaScript – z których większość nie została napisana w TypeScript. Jak nasz ściśle typowany kod TypeScript rozumie kształty, funkcje i zmienne z nietypizowanej biblioteki JavaScript?
Odpowiedź tkwi w plikach deklaracji TypeScript. Pliki te, identyfikowalne po rozszerzeniu .d.ts, są zasadniczym mostem między światami TypeScript i JavaScript. Działają jako plan lub umowa API, opisując typy biblioteki innej firmy bez zawierania żadnej jej faktycznej implementacji. W tym kompleksowym przewodniku omówimy wszystko, co musisz wiedzieć, aby z pewnością zarządzać definicjami typów dla dowolnej biblioteki JavaScript w swoich projektach TypeScript.
Czym dokładnie są pliki deklaracji TypeScript?
Wyobraź sobie, że zatrudniłeś wykonawcę, który mówi tylko innym językiem. Aby skutecznie z nim współpracować, potrzebujesz tłumacza lub szczegółowego zestawu instrukcji w języku, który obydwaj rozumiecie. Plik deklaracji służy dokładnie temu samemu celowi dla kompilatora TypeScript (wykonawcy).
Plik .d.ts zawiera tylko informacje o typach. Obejmuje on:
- Sygnatury funkcji i metod (typy parametrów, typy zwracane).
- Definicje zmiennych i ich typów.
- Interfejsy i aliasy typów dla złożonych obiektów.
- Definicje klas, w tym ich właściwości i metody.
- Struktury przestrzeni nazw i modułów.
Kluczowe jest to, że pliki te nie zawierają żadnego wykonywalnego kodu. Służą one wyłącznie do analizy statycznej. Gdy importujesz bibliotekę JavaScript, taką jak Lodash, do swojego projektu TypeScript, kompilator szuka odpowiedniego pliku deklaracji. Jeśli go znajdzie, może zweryfikować kod, zapewnić inteligentne autouzupełnianie i upewnić się, że używasz biblioteki poprawnie. Jeśli go nie znajdzie, zgłosi błąd, taki jak: Nie można znaleźć pliku deklaracji dla modułu 'lodash'.
Dlaczego pliki deklaracji są niezbywalne dla profesjonalnego rozwoju
Używanie bibliotek JavaScript bez odpowiednich definicji typów w projekcie TypeScript podważa sam powód używania TypeScript w pierwszej kolejności. Rozważmy prosty scenariusz z użyciem popularnej biblioteki narzędziowej Lodash.
Świat bez definicji typów
Bez pliku deklaracji TypeScript nie ma pojęcia, czym jest lodash i co zawiera. Aby nawet zmusić kod do kompilacji, możesz ulec pokusie użycia szybkiej poprawki, takiej jak ta:
const _: any = require('lodash');
const users = [{ 'user': 'barney' }, { 'user': 'fred' }];
// Autouzupełnianie? Żadnej pomocy tutaj.
// Sprawdzanie typów? Nie. Czy 'username' jest właściwą właściwością?
// Kompilator na to pozwala, ale może się nie powieść w czasie wykonywania.
W tym przypadku zmienna _ jest typu any. Skutecznie mówi to TypeScript: „Nie sprawdzaj niczego związanego z tą zmienną”. Tracisz wszystkie korzyści: brak autouzupełniania, brak sprawdzania typów argumentów i brak pewności co do typu zwracanego. To wylęgarnia błędów w czasie wykonywania.
Świat z definicjami typów
Zobaczmy teraz, co się dzieje, gdy dostarczymy niezbędny plik deklaracji. Po zainstalowaniu typów (które omówimy w następnej kolejności), doświadczenie zostaje zmienione:
import _ from 'lodash';
interface User {
user: string;
active?: boolean;
}
const users: User[] = [{ 'user': 'barney' }, { 'user': 'fred' }];
// 1. Edytor zapewnia autouzupełnianie dla 'find' i innych funkcji lodash.
// 2. Najechanie na 'find' pokazuje jego pełną sygnaturę i dokumentację.
// 3. TypeScript widzi, że `users` to tablica obiektów `User`.
// 4. TypeScript wie, że predykat dla `find` na `User[]` powinien obejmować `user` lub `active`.
// POPRAWNIE: TypeScript jest zadowolony.
const fred = _.find(users, { user: 'fred' });
// BŁĄD: TypeScript łapie błąd!
// Właściwość 'username' nie istnieje w typie 'User'.
const betty = _.find(users, { username: 'betty' });
Różnica jest ogromna. Zyskujemy pełne bezpieczeństwo typów, doskonałe doświadczenie programistyczne dzięki narzędziom i dramatyczną redukcję potencjalnych błędów. To profesjonalny standard pracy z TypeScript.
Hierarchia wyszukiwania definicji typów
Jak więc zdobyć te magiczne pliki .d.ts dla swoich ulubionych bibliotek? Istnieje dobrze ugruntowany proces, który obejmuje zdecydowaną większość scenariuszy.
Krok 1: Sprawdź, czy biblioteka łączy własne typy
Najlepszy scenariusz to taki, gdy biblioteka jest napisana w TypeScript lub jej konserwatorzy dostarczają oficjalne pliki deklaracji w tym samym pakiecie. Jest to coraz bardziej powszechne w przypadku nowoczesnych, dobrze utrzymanych projektów.
Jak sprawdzić:
- Zainstaluj bibliotekę jak zwykle:
npm install axios - Poszukaj wewnątrz folderu biblioteki w
node_modules/axios. Czy widzisz jakieś pliki.d.ts? - Sprawdź plik
package.jsonbiblioteki pod kątem pola"types"lub"typings". To pole wskazuje bezpośrednio na główny plik deklaracji. Na przykładpackage.jsonAxios zawiera:"types": "index.d.ts".
Jeśli te warunki są spełnione, gotowe! TypeScript automatycznie znajdzie i użyje tych dołączonych typów. Dalsze działania nie są wymagane.
Krok 2: Projekt DefinitelyTyped (@types)
Dla tysięcy bibliotek JavaScript, które nie dołączają własnych typów, globalna społeczność TypeScript stworzyła niesamowite źródło: DefinitelyTyped.
DefinitelyTyped to scentralizowane, zarządzane przez społeczność repozytorium na GitHub, które hostuje wysokiej jakości pliki deklaracji dla ogromnej liczby pakietów JavaScript. Te definicje są publikowane w rejestrze npm w zakresie @types.
Jak z tego korzystać:
Jeśli biblioteka, taka jak lodash, nie dołącza własnych typów, po prostu zainstaluj odpowiedni pakiet @types jako zależność programistyczną:
npm install --save-dev @types/lodash
Konwencja nazewnictwa jest prosta i przewidywalna: dla pakietu o nazwie nazwa-pakietu, jego typy będą prawie zawsze w @types/nazwa-pakietu. Możesz wyszukać dostępne typy na stronie internetowej npm lub bezpośrednio w repozytorium DefinitelyTyped.
Dlaczego --save-dev? Pliki deklaracji są potrzebne tylko podczas tworzenia i kompilacji. Nie zawierają żadnego kodu w czasie wykonywania, więc nie powinny być dołączane do ostatecznego pakietu produkcyjnego. Zainstalowanie ich jako devDependency zapewnia to oddzielenie.
Krok 3: Gdy nie istnieją żadne typy - pisanie własnych
Co jeśli używasz starszej, niszowej lub wewnętrznej biblioteki prywatnej, która nie dołącza typów i nie znajduje się w DefinitelyTyped? W takim przypadku musisz zakasać rękawy i utworzyć własny plik deklaracji. Chociaż może to brzmieć przerażająco, możesz zacząć prosto i dodawać więcej szczegółów w razie potrzeby.
Szybka poprawka: Deklaracja modułu otoczenia skrótu
Czasami po prostu potrzebujesz, aby Twój projekt kompilował się bez błędów, podczas gdy wymyślasz odpowiednią strategię typowania. Możesz utworzyć plik w swoim projekcie (np. declarations.d.ts lub types/global.d.ts) i dodać deklarację skrótu:
// w pliku .d.ts
declare module 'some-untyped-library';
To mówi TypeScript: „Ufaj mi, istnieje moduł o nazwie „some-untyped-library”. Po prostu traktuj wszystko zaimportowane z niego jako typ any.” To wycisza błąd kompilatora, ale jak omówiliśmy, poświęca całe bezpieczeństwo typów dla tej biblioteki. To tymczasowa poprawka, a nie rozwiązanie długoterminowe.
Tworzenie podstawowego niestandardowego pliku deklaracji
Lepszym podejściem jest rozpoczęcie definiowania typów dla tych części biblioteki, których faktycznie używasz. Powiedzmy, że mamy prostą bibliotekę o nazwie `string-utils`, która eksportuje pojedynczą funkcję.
// W node_modules/string-utils/index.js
module.exports.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
Możemy utworzyć plik string-utils.d.ts w dedykowanym katalogu `types` w katalogu głównym naszego projektu.
// W mo-projekcie/types/string-utils.d.ts
declare module 'string-utils' {
export function capitalize(str: string): string;
// Możesz dodać tutaj inne definicje funkcji w miarę ich używania
// export function slugify(str: string): string;
}
Teraz musimy powiedzieć TypeScript, gdzie znaleźć nasze niestandardowe definicje typów. Robimy to w tsconfig.json:
{
"compilerOptions": {
// ... inne opcje
"baseUrl": ".",
"paths": {
"*": ["types/*"]
}
}
}
Dzięki tej konfiguracji, gdy import { capitalize } from 'string-utils', TypeScript znajdzie Twój niestandardowy plik deklaracji i zapewni bezpieczeństwo typów, które zdefiniowałeś. Możesz stopniowo budować ten plik w miarę korzystania z większej liczby funkcji biblioteki.
Zagłębianie się: Tworzenie plików deklaracji
Przeanalizujmy kilka bardziej zaawansowanych koncepcji, z którymi spotkasz się podczas pisania lub czytania plików deklaracji.
Deklarowanie różnych rodzajów eksportów
Moduły JavaScript mogą eksportować rzeczy na różne sposoby. Twój plik deklaracji musi pasować do struktury eksportu biblioteki.
- Eksporty nazwane: To najczęstszy przypadek. Widzieliśmy to powyżej z
export function capitalize(...). Możesz także eksportować stałe, interfejsy i klasy. - Eksport domyślny: Dla bibliotek, które używają
export default. - Globalne UMD: W przypadku starszych bibliotek zaprojektowanych do działania w przeglądarkach za pomocą tagu
<script>, często dołączają się one do globalnego obiektuwindow. Możesz zadeklarować te zmienne globalne. export =iimport = require(): Ta składnia jest przeznaczona dla starszych modułów CommonJS, które używająmodule.exports = .... Na przykład, jeśli biblioteka wykonujemodule.exports = myClass;.
declare module 'my-lib' {
export const version: string;
export interface Options { retries: number; }
export function doSomething(options: Options): Promise
declare module 'my-default-lib' {
// Dla domyślnego eksportu funkcji
export default function myCoolFunction(): void;
// Dla domyślnego eksportu obiektu
// const myLib = { name: 'lib', version: '1.0' };
// export default myLib;
}
// Deklaruje zmienną globalną '$' określonego typu
declare var $: JQueryStatic;
// w my-class.d.ts
declare class MyClass { constructor(name: string); }
export = MyClass;
// w twojej aplikacji.ts
import MyClass = require('my-class');
const instance = new MyClass('test');
Chociaż mniej powszechne w przypadku nowoczesnych modułów ES, jest to kluczowe dla kompatybilności z wieloma starszymi, ale nadal szeroko używanymi pakietami Node.js.
Rozszerzanie modułu: Rozszerzanie istniejących typów
Jedną z najpotężniejszych funkcji jest rozszerzanie modułu (znane również jako scalanie deklaracji). Umożliwia to dodawanie właściwości do istniejących interfejsów zdefiniowanych w pliku deklaracji innego pakietu. Jest to niezwykle przydatne w przypadku bibliotek z architekturą wtyczek, takich jak Express lub Fastify.
Wyobraź sobie, że używasz oprogramowania pośredniczącego w Express, które dodaje właściwość user do obiektu Request. Bez rozszerzania TypeScript zgłosi, że user nie istnieje w Request.
Oto jak możesz poinformować TypeScript o tej nowej właściwości:
// w pliku types/express.d.ts
// Musimy zaimportować oryginalny typ, aby go rozszerzyć
import { UserProfile } from './auth'; // Zakładając, że masz typ UserProfile
// Powiedz TypeScript, że rozszerzamy moduł 'express-serve-static-core'
declare module 'express-serve-static-core' {
// Skieruj interfejs 'Request' wewnątrz tego modułu
interface Request {
// Dodaj naszą niestandardową właściwość
user?: UserProfile;
}
}
Teraz, w całej aplikacji, obiekt Express Request będzie poprawnie typowany z opcjonalną właściwością user i uzyskasz pełne bezpieczeństwo typów i autouzupełnianie.
Dyrektywy potrójnego ukośnika
Czasami możesz zobaczyć komentarze na górze plików .d.ts, które zaczynają się od trzech ukośników (///). Są to dyrektywy potrójnego ukośnika, które działają jako instrukcje kompilatora.
/// <reference types="..." />: Jest to najczęstszy. Jawnie zawiera definicje typów innego pakietu jako zależności. Na przykład typy dla wtyczki WebdriverIO mogą zawierać/// <reference types="webdriverio" />, ponieważ ich własne typy zależą od typów podstawowych WebdriverIO./// <reference path="..." />: Służy do deklarowania zależności od innego pliku w tym samym projekcie. Jest to starsza składnia, w dużej mierze zastąpiona importami modułów ES.
Najlepsze praktyki dotyczące zarządzania plikami deklaracji
- Preferuj typy dołączone: Przy wyborze bibliotek preferuj te, które są napisane w TypeScript lub dołączają własne oficjalne definicje typów. Sygnalizuje to zaangażowanie w ekosystem TypeScript.
- Przechowuj
@typeswdevDependencies: Zawsze instaluj pakiety@typesza pomocą--save-devlub-D. Nie są one potrzebne dla kodu produkcyjnego. - Dopasuj wersje: Częstym źródłem błędów jest niedopasowanie między wersją biblioteki a jej wersją
@types. Duży skok wersji w bibliotece (np. z v2 do v3) prawdopodobnie będzie miał zmiany powodujące zakłócenia w jej API, co musi znaleźć odzwierciedlenie w pakiecie@types. Spróbuj je synchronizować. - Użyj
tsconfig.jsondo kontroli: Opcje kompilatoratypeRootsitypesw plikutsconfig.jsonmogą zapewnić precyzyjną kontrolę nad tym, gdzie TypeScript szuka plików deklaracji.typeRootsinformuje kompilator, które foldery ma sprawdzić (domyślnie jest to./node_modules/@types), atypespozwala jawnie wymienić pakiety typów, które mają zostać uwzględnione. - Wprowadź wkład: Jeśli napiszesz kompleksowy plik deklaracji dla biblioteki, która go nie posiada, rozważ wniesienie wkładu do projektu DefinitelyTyped. To fantastyczny sposób na oddanie społeczności programistów i pomoc tysiącom innych osób.
Podsumowanie: Niewyczesani bohaterowie bezpieczeństwa typów
Pliki deklaracji TypeScript są niewyczesanymi bohaterami, które umożliwiają bezproblemową integrację dynamicznego, rozległego świata JavaScript z solidnym, bezpiecznym typowo środowiskiem programistycznym. Są kluczowym ogniwem, które wzmacnia nasze narzędzia, zapobiega niezliczonym błędom i sprawia, że nasze bazy kodu są bardziej elastyczne i samoudokumentowane.
Rozumiejąc, jak znaleźć, używać, a nawet tworzyć własne pliki .d.ts, nie tylko naprawiasz błąd kompilatora — podnosisz cały przepływ pracy programistycznej. Odblokowujesz pełny potencjał zarówno TypeScript, jak i bogatego ekosystemu bibliotek JavaScript, tworząc potężną synergię, która skutkuje lepszym, bardziej niezawodnym oprogramowaniem dla globalnej publiczności.